/*-------------------------------------------------------------------------
 *
 * timeline.c
 *		读取和写入时间线历史文件的功能。
 *
 * 时间线历史文件以简单的文本格式列出时间线的变化。它们与 WAL 段一起
 * 存档。
 *
 * 文件命名为 "<tli>.history"。例如，如果数据库启动并切换到时间线 5，
 * 则时间线历史文件将被称为 "00000005.history"。
 *
 * 文件中的每一行表示一个时间线切换：
 *
 * <parentTLI> <switchpoint> <reason>
 *
 *	parentTLI	父时间线的 ID
 *	switchpoint 时间切换发生时的 WAL 位置的 XLogRecPtr
 *	reason		时间线更改原因的人类可读解释
 *
 * 字段用制表符分隔。以 # 开头的行是注释，并被忽略。空行也会被忽略。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * src/backend/access/transam/timeline.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <sys/stat.h>
#include <unistd.h>

#include "access/timeline.h"
#include "access/xlog.h"
#include "access/xlog_internal.h"
#include "access/xlogarchive.h"
#include "access/xlogdefs.h"
#include "pgstat.h"
#include "storage/fd.h"

/*
 * 复制归档中 ID 范围在 'begin' 和 'end' 之间的所有时间线历史文件到 pg_wal。
 */
void restoreTimeLineHistoryFiles(TimeLineID fc_begin, TimeLineID fc_end)
{
	char		fc_path[MAXPGPATH];
	char		fc_histfname[MAXFNAMELEN];
	TimeLineID	fc_tli;

	for (fc_tli = fc_begin; fc_tli < fc_end; fc_tli++)
	{
		if (fc_tli == 1)
			continue;

		TLHistoryFileName(fc_histfname, fc_tli);
		if (RestoreArchivedFile(fc_path, fc_histfname, "RECOVERYHISTORY", 0, false))
			KeepFileRestoredFromArchive(fc_path, fc_histfname);
	}
}

/*
 * 尝试读取时间线的历史文件。
 *
 * 如果成功，返回组件 TLI 的列表（给定 TLI 及其祖先 TLI）。如果找不到历史文件，假设该
 * 时间线没有父级，并返回仅包含指定时间线 ID 的列表。
 */
List * readTimeLineHistory(TimeLineID fc_targetTLI)
{
	List	   *fc_result;
	char		fc_path[MAXPGPATH];
	char		fc_histfname[MAXFNAMELEN];
	FILE	   *fc_fd;
	TimeLineHistoryEntry *fc_entry;
	TimeLineID	fc_lasttli = 0;
	XLogRecPtr	fc_prevend;
	bool		fc_fromArchive = false;

	/* 时间线 1 没有历史文件，因此无需检查 */
	if (fc_targetTLI == 1)
	{
		fc_entry = (TimeLineHistoryEntry *) palloc(sizeof(TimeLineHistoryEntry));
		fc_entry->tli = fc_targetTLI;
		fc_entry->begin = fc_entry->end = InvalidXLogRecPtr;
		return list_make1(fc_entry);
	}

	if (ArchiveRecoveryRequested)
	{
		TLHistoryFileName(fc_histfname, fc_targetTLI);
		fc_fromArchive =
			RestoreArchivedFile(fc_path, fc_histfname, "RECOVERYHISTORY", 0, false);
	}
	else
		TLHistoryFilePath(fc_path, fc_targetTLI);

	fc_fd = AllocateFile(fc_path, "r");
	if (fc_fd == NULL)
	{
		if (errno != ENOENT)
			ereport(FATAL,
					(errcode_for_file_access(),
					 errmsg("could not open file \"%s\": %m", fc_path)));
		/* 不存在，因此假设没有父级 */
		fc_entry = (TimeLineHistoryEntry *) palloc(sizeof(TimeLineHistoryEntry));
		fc_entry->tli = fc_targetTLI;
		fc_entry->begin = fc_entry->end = InvalidXLogRecPtr;
		return list_make1(fc_entry);
	}

	fc_result = NIL;

	/*
	 * 解析文件...
	 */
	fc_prevend = InvalidXLogRecPtr;
	for (;;)
	{
		char		fc_fline[MAXPGPATH];
		char	   *fc_res;
		char	   *fc_ptr;
		TimeLineID	fc_tli;
		uint32		fc_switchpoint_hi;
		uint32		fc_switchpoint_lo;
		int			fc_nfields;

		pgstat_report_wait_start(WAIT_EVENT_TIMELINE_HISTORY_READ);
		fc_res = fgets(fc_fline, sizeof(fc_fline), fc_fd);
		pgstat_report_wait_end();
		if (fc_res == NULL)
		{
			if (ferror(fc_fd))
				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not read file \"%s\": %m", fc_path)));

			break;
		}

		/* 跳过前导空格并检查 # 注释 */
		for (fc_ptr = fc_fline; *fc_ptr; fc_ptr++)
		{
			if (!isspace((unsigned char) *fc_ptr))
				break;
		}
		if (*fc_ptr == '\0' || *fc_ptr == '#')
			continue;

		fc_nfields = sscanf(fc_fline, "%u\t%X/%X", &fc_tli, &fc_switchpoint_hi, &fc_switchpoint_lo);

		if (fc_nfields < 1)
		{
			/* 期望数字时间线 ID 作为行的第一个字段 */
			ereport(FATAL,
					(errmsg("syntax error in history file: %s", fc_fline),
					 errhint("Expected a numeric timeline ID.")));
		}
		if (fc_nfields != 3)
			ereport(FATAL,
					(errmsg("syntax error in history file: %s", fc_fline),
					 errhint("Expected a write-ahead log switchpoint location.")));

		if (fc_result && fc_tli <= fc_lasttli)
			ereport(FATAL,
					(errmsg("invalid data in history file: %s", fc_fline),
					 errhint("Timeline IDs must be in increasing sequence.")));

		fc_lasttli = fc_tli;

		fc_entry = (TimeLineHistoryEntry *) palloc(sizeof(TimeLineHistoryEntry));
		fc_entry->tli = fc_tli;
		fc_entry->begin = fc_prevend;
		fc_entry->end = ((uint64) (fc_switchpoint_hi)) << 32 | (uint64) fc_switchpoint_lo;
		fc_prevend = fc_entry->end;

		/* 创建一个最新项在前的列表 */
		fc_result = lcons(fc_entry, fc_result);

		/* 我们忽略每行的其余部分 */
	}

	FreeFile(fc_fd);

	if (fc_result && fc_targetTLI <= fc_lasttli)
		ereport(FATAL,
				(errmsg("invalid data in history file \"%s\"", fc_path),
				 errhint("Timeline IDs must be less than child timeline's ID.")));

	/*
	 * 为时间线的“尖端”创建一个额外条目，该条目在历史文件中没有条目。
	 */
	fc_entry = (TimeLineHistoryEntry *) palloc(sizeof(TimeLineHistoryEntry));
	fc_entry->tli = fc_targetTLI;
	fc_entry->begin = fc_prevend;
	fc_entry->end = InvalidXLogRecPtr;

	fc_result = lcons(fc_entry, fc_result);

	/*
	 * 如果历史文件是从归档中获取的，请将其保存在 pg_wal 以供将来参考。
	 */
	if (fc_fromArchive)
		KeepFileRestoredFromArchive(fc_path, fc_histfname);

	return fc_result;
}

/*
 * 探测给定时间线 ID 是否存在时间线历史文件
 */
bool existsTimeLineHistory(TimeLineID fc_probeTLI)
{
	char		fc_path[MAXPGPATH];
	char		fc_histfname[MAXFNAMELEN];
	FILE	   *fc_fd;

	/* 时间线 1 没有历史文件，因此无需检查 */
	if (fc_probeTLI == 1)
		return false;

	if (ArchiveRecoveryRequested)
	{
		TLHistoryFileName(fc_histfname, fc_probeTLI);
		RestoreArchivedFile(fc_path, fc_histfname, "RECOVERYHISTORY", 0, false);
	}
	else
		TLHistoryFilePath(fc_path, fc_probeTLI);

	fc_fd = AllocateFile(fc_path, "r");
	if (fc_fd != NULL)
	{
		FreeFile(fc_fd);
		return true;
	}
	else
	{
		if (errno != ENOENT)
			ereport(FATAL,
					(errcode_for_file_access(),
					 errmsg("could not open file \"%s\": %m", fc_path)));
		return false;
	}
}

/*
 * 找到最新的现有时间线，假设 startTLI 存在。
 *
 * 注意：虽然这有些启发式，但它确实积极保证 (result + 1) 不是一个已知的时间线，因此
 * 应该安全地将该 ID 分配给新的时间线。
 */
TimeLineID findNewestTimeLine(TimeLineID fc_startTLI)
{
	TimeLineID	fc_newestTLI;
	TimeLineID	fc_probeTLI;

	/*
	 * 该算法仅仅是探测时间线历史文件的存在。 XXX 允许序列中出现间隙是否有用？
	 */
	fc_newestTLI = fc_startTLI;

	for (fc_probeTLI = fc_startTLI + 1;; fc_probeTLI++)
	{
		if (existsTimeLineHistory(fc_probeTLI))
		{
			fc_newestTLI = fc_probeTLI;	/* probeTLI 存在 */
		}
		else
		{
			/* 不存在，假设我们完成了 */
			break;
		}
	}

	return fc_newestTLI;
}

/*
 * 创建一个新的时间线历史文件。
 *
 *	newTLI：新时间线的 ID
 *	parentTLI：其直接父级的 ID
 *	switchpoint：系统切换到新时间线的 WAL 位置
 *	reason：对时间线切换原因的可读性解释
 *
 * 目前这仅在结束恢复时使用，因此没有锁定考虑。但我们应该和 XLogFileInit 一样紧张，以避
 *免放置一个虚假的文件。
 */
void writeTimeLineHistory(TimeLineID fc_newTLI, TimeLineID fc_parentTLI,
					 XLogRecPtr fc_switchpoint, char *fc_reason)
{
	char		fc_path[MAXPGPATH];
	char		fc_tmppath[MAXPGPATH];
	char		fc_histfname[MAXFNAMELEN];
	char		fc_buffer[BLCKSZ];
	int			fc_srcfd;
	int			fc_fd;
	int			fc_nbytes;

	Assert(fc_newTLI > fc_parentTLI); /* 否则选择新时间线 ID 有误 */

	/*
	 * 写入临时文件名。
	 */
	snprintf(fc_tmppath, MAXPGPATH, XLOGDIR "/xlogtemp.%d", (int) getpid());

	unlink(fc_tmppath);

	/* 不要在这里使用 get_sync_bit() --- 只想在填充结束时进行 fsync */
	fc_fd = OpenTransientFile(fc_tmppath, O_RDWR | O_CREAT | O_EXCL);
	if (fc_fd < 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not create file \"%s\": %m", fc_tmppath)));

	/*
	 * 如果存在父级的历史文件，请逐字复制它
	 */
	if (ArchiveRecoveryRequested)
	{
		TLHistoryFileName(fc_histfname, fc_parentTLI);
		RestoreArchivedFile(fc_path, fc_histfname, "RECOVERYHISTORY", 0, false);
	}
	else
		TLHistoryFilePath(fc_path, fc_parentTLI);

	fc_srcfd = OpenTransientFile(fc_path, O_RDONLY);
	if (fc_srcfd < 0)
	{
		if (errno != ENOENT)
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not open file \"%s\": %m", fc_path)));
		/* 不存在，因此假设父级没有父级 */
	}
	else
	{
		for (;;)
		{
			errno = 0;
			pgstat_report_wait_start(WAIT_EVENT_TIMELINE_HISTORY_READ);
			fc_nbytes = (int) read(fc_srcfd, fc_buffer, sizeof(fc_buffer));
			pgstat_report_wait_end();
			if (fc_nbytes < 0 || errno != 0)
				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not read file \"%s\": %m", fc_path)));
			if (fc_nbytes == 0)
				break;
			errno = 0;
			pgstat_report_wait_start(WAIT_EVENT_TIMELINE_HISTORY_WRITE);
			if ((int) write(fc_fd, fc_buffer, fc_nbytes) != fc_nbytes)
			{
				int			fc_save_errno = errno;

				/*
				 * 如果我们未能创建文件，请删除它以释放磁盘空间
				 */
				unlink(fc_tmppath);

				/*
				 * 如果写入未设置 errno，假设问题是没有磁盘空间
				 */
				errno = fc_save_errno ? fc_save_errno : ENOSPC;

				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not write to file \"%s\": %m", fc_tmppath)));
			}
			pgstat_report_wait_end();
		}

		if (CloseTransientFile(fc_srcfd) != 0)
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not close file \"%s\": %m", fc_path)));
	}

	/*
	 * 附加一行关于此时间线分割的详细信息。
	 *
	 * 如果我们确实有父文件，则插入一个额外的换行，以防父文件未以换行结束。
	 */
	snprintf(fc_buffer, sizeof(fc_buffer),
			 "%s%u\t%X/%X\t%s\n",
			 (fc_srcfd < 0) ? "" : "\n",
			 fc_parentTLI,
			 LSN_FORMAT_ARGS(fc_switchpoint),
			 fc_reason);

	fc_nbytes = strlen(fc_buffer);
	errno = 0;
	pgstat_report_wait_start(WAIT_EVENT_TIMELINE_HISTORY_WRITE);
	if ((int) write(fc_fd, fc_buffer, fc_nbytes) != fc_nbytes)
	{
		int			fc_save_errno = errno;

		/*
		 * 如果我们未能创建文件，请删除它以释放磁盘空间
		 */
		unlink(fc_tmppath);
		/* 如果写入未设置 errno，假设问题是没有磁盘空间 */
		errno = fc_save_errno ? fc_save_errno : ENOSPC;

		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not write to file \"%s\": %m", fc_tmppath)));
	}
	pgstat_report_wait_end();

	pgstat_report_wait_start(WAIT_EVENT_TIMELINE_HISTORY_SYNC);
	if (pg_fsync(fc_fd) != 0)
		ereport(data_sync_elevel(ERROR),
				(errcode_for_file_access(),
				 errmsg("could not fsync file \"%s\": %m", fc_tmppath)));
	pgstat_report_wait_end();

	if (CloseTransientFile(fc_fd) != 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not close file \"%s\": %m", fc_tmppath)));

	/*
	 * 现在将完成的历史文件移动到其最终名称。
	 */
	TLHistoryFilePath(fc_path, fc_newTLI);
	Assert(access(fc_path, F_OK) != 0 && errno == ENOENT);
	durable_rename(fc_tmppath, fc_path, ERROR);

	/* 历史文件可以立即归档。 */
	if (XLogArchivingActive())
	{
		TLHistoryFileName(fc_histfname, fc_newTLI);
		XLogArchiveNotify(fc_histfname);
	}
}

/*
 * 为给定的时间线和内容写入历史文件。
 *
 * 目前这仅在 walreceiver 进程中使用，因此没有锁定考虑。
 * 但是我们应该和 XLogFileInit 一样紧张，以避免放置一个虚假的文件。
 */
void writeTimeLineHistoryFile(TimeLineID fc_tli, char *fc_content, int fc_size)
{
	char		fc_path[MAXPGPATH];
	char		fc_tmppath[MAXPGPATH];
	int			fc_fd;

	/*
	 * 写入临时文件名。
	 */
	snprintf(fc_tmppath, MAXPGPATH, XLOGDIR "/xlogtemp.%d", (int) getpid());

	unlink(fc_tmppath);

	/* 不要在这里使用 get_sync_bit() --- 只想在填充结束时进行 fsync */
	fc_fd = OpenTransientFile(fc_tmppath, O_RDWR | O_CREAT | O_EXCL);
	if (fc_fd < 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not create file \"%s\": %m", fc_tmppath)));

	errno = 0;
	pgstat_report_wait_start(WAIT_EVENT_TIMELINE_HISTORY_FILE_WRITE);
	if ((int) write(fc_fd, fc_content, fc_size) != fc_size)
	{
		int			fc_save_errno = errno;

		/*
		 * 如果我们未能创建文件，请删除它以释放磁盘空间
		 */
		unlink(fc_tmppath);
		/* 如果写入未设置 errno，假设问题是没有磁盘空间 */
		errno = fc_save_errno ? fc_save_errno : ENOSPC;

		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not write to file \"%s\": %m", fc_tmppath)));
	}
	pgstat_report_wait_end();

	pgstat_report_wait_start(WAIT_EVENT_TIMELINE_HISTORY_FILE_SYNC);
	if (pg_fsync(fc_fd) != 0)
		ereport(data_sync_elevel(ERROR),
				(errcode_for_file_access(),
				 errmsg("could not fsync file \"%s\": %m", fc_tmppath)));
	pgstat_report_wait_end();

	if (CloseTransientFile(fc_fd) != 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not close file \"%s\": %m", fc_tmppath)));

	/*
	 * 现在将完成的历史文件移入位置，并使用其最终名称，
	 * 替换任何具有相同名称的现有文件。
	 */
	TLHistoryFilePath(fc_path, fc_tli);
	durable_rename(fc_tmppath, fc_path, ERROR);
}

/*
 * 如果 'expectedTLEs' 包含 id 为 'tli' 的时间线，则返回 true
 */
bool tliInHistory(TimeLineID fc_tli, List *fc_expectedTLEs)
{
	ListCell   *fc_cell;

	foreach(fc_cell, fc_expectedTLEs)
	{
		if (((TimeLineHistoryEntry *) lfirst(fc_cell))->tli == fc_tli)
			return true;
	}

	return false;
}

/*
 * 返回在给定时间线历史中，在特定时间点使用的时间线的 ID。
 */
TimeLineID tliOfPointInHistory(XLogRecPtr fc_ptr, List *fc_history)
{
	ListCell   *fc_cell;

	foreach(fc_cell, fc_history)
	{
		TimeLineHistoryEntry *fc_tle = (TimeLineHistoryEntry *) lfirst(fc_cell);

		if ((XLogRecPtrIsInvalid(fc_tle->begin) || fc_tle->begin <= fc_ptr) &&
			(XLogRecPtrIsInvalid(fc_tle->end) || fc_ptr < fc_tle->end))
		{
			/* 找到了 */
			return fc_tle->tli;
		}
	}

	/* 不应该发生。 */
	elog(ERROR, "timeline history was not contiguous");
	return 0;					/* 保持编译器安静 */
}

/*
 * 返回我们在给定时间线中分支的历史时刻，
 * 以及我们分支到的时间线 (*nextTLI)。如果时间线当前，则返回 InvalidXLogRecPtr，
 * 即我们没有从中分支，并且如果时间线不是该服务器历史的一部分，则抛出错误。
 */
XLogRecPtr tliSwitchPoint(TimeLineID fc_tli, List *fc_history, TimeLineID *fc_nextTLI)
{
	ListCell   *fc_cell;

	if (fc_nextTLI)
		*fc_nextTLI = 0;
	foreach(fc_cell, fc_history)
	{
		TimeLineHistoryEntry *fc_tle = (TimeLineHistoryEntry *) lfirst(fc_cell);

		if (fc_tle->tli == fc_tli)
			return fc_tle->end;
		if (fc_nextTLI)
			*fc_nextTLI = fc_tle->tli;
	}

	ereport(ERROR,
			(errmsg("requested timeline %u is not in this server's history",
					fc_tli)));
	return InvalidXLogRecPtr;	/* 保持编译器安静 */
}
